package cn.sciento.core.algorithm.structure;

import cn.sciento.core.util.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

public class TTLMap<K, V> implements Map<K, V> {
    protected Map<K, Pair<V, Long>> innerMap;

    protected TimeUnit timeUnit;

    protected long ttl;

    private long ttlInMillisecond;

    private static final Class<? extends Map> DEFAULT_INNER_MAP_CLASS =  HashMap.class;

    private static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.MINUTES;

    private static final long DEFAULT_TTL = 10L;

    public TTLMap() {
        initWithDefaultValue();
    }

    private TTLMap(boolean isBuildeMode) {
        if (!isBuildeMode)
            initWithDefaultValue();
    }

    public int size() {
        clearTimeoutKeys();
        return this.innerMap.size();
    }

    public boolean isEmpty() {
        clearTimeoutKeys();
        return this.innerMap.isEmpty();
    }

    public boolean containsKey(Object key) {
        if (clearTimeoutKey(key))
            return false;
        return this.innerMap.containsKey(key);
    }

    public boolean containsValue(Object value) {
        clearTimeoutKeys();
        if (this.innerMap.isEmpty())
            return false;
        return this.innerMap.values().stream().map(Pair::getFirst).anyMatch(v -> Objects.equals(v, value));
    }

    public V get(Object key) {
        return getTrueValue(key);
    }

    public V put(K key, V value) {
        this.innerMap.put(key, new Pair(value, System.currentTimeMillis() + this.ttlInMillisecond));
        return value;
    }

    public V put(K key, V value, long ttl) {
        this.innerMap.put(key, new Pair(value, System.currentTimeMillis() + this.timeUnit.toMillis(ttl)));
        return value;
    }

    public V put(K key, V value, long ttl, TimeUnit timeUnit) {
        if (timeUnit == null)
            timeUnit = this.timeUnit;
        this.innerMap.put(key, new Pair(value, Long.valueOf(System.currentTimeMillis() + timeUnit.toMillis(ttl))));
        return value;
    }

    public V remove(Object key) {
        if (!containsKey(key))
            return null;
        V value = getTrueValue(key);
        this.innerMap.remove(key);
        return value;
    }

    public void putAll(Map<? extends K, ? extends V> m) {
        if (m == null || m.isEmpty())
            return;
        for (Entry<? extends K, ? extends V> entry : m.entrySet())
            put(entry.getKey(), entry.getValue());
    }

    public void clear() {
        this.innerMap.clear();
    }

    public Set<K> keySet() {
        clearTimeoutKeys();
        if (this.innerMap.isEmpty())
            return Collections.emptySet();
        return this.innerMap.keySet();
    }

    public Collection<V> values() {
        clearTimeoutKeys();
        if (this.innerMap.isEmpty())
            return Collections.emptyList();
        return (Collection<V>) this.innerMap.values().stream().map(Pair::getFirst).collect(Collectors.toList());
    }


    public Set<Entry<K, V>> entrySet() {
        this.clearTimeoutKeys();
        return this.innerMap.isEmpty() ? Collections.emptySet() : (Set)this.innerMap.entrySet().stream().map((entry) -> {
            return new TTLMap.InnerEntry(entry.getKey(), Optional.ofNullable(entry.getValue()).map(Pair::getFirst).orElse((V) null));
        }).collect(Collectors.toSet());
    }

    protected int clearTimeoutKeys() {
        if (this.innerMap.isEmpty())
            return 0;
        long nowTimestamp = System.currentTimeMillis();
        Set<K> keySet = (Set<K>) this.innerMap.keySet().stream().filter(key -> isTimeOut(key, nowTimestamp)).collect(Collectors.toSet());
        keySet.forEach(this.innerMap::remove);
        return keySet.size();
    }

    protected boolean clearTimeoutKey(Object key) {
        if (this.innerMap.isEmpty())
            return false;
        if (isTimeOut(key, System.currentTimeMillis())) {
            this.innerMap.remove(key);
            return true;
        }
        return false;
    }

    protected boolean isTimeOut(Object key, long nowTimestamp) {
        if (this.innerMap.isEmpty())
            return true;
        Pair<V, Long> pair = this.innerMap.get(key);
        if (pair == null)
            return true;
        return (((Long) pair.getSecond()).longValue() < nowTimestamp);
    }

    protected V getTrueValue(Object key) {
        if (this.innerMap.isEmpty())
            return null;
        return clearTimeoutKey(key) ? null : Optional.ofNullable(this.innerMap.get(key)).map(Pair::getFirst).orElse(null);
    }

    private void initWithDefaultValue() {
        this.innerMap = new HashMap<>();
        this.timeUnit = DEFAULT_TIME_UNIT;
        this.ttl = 10L;
        this.ttlInMillisecond = this.timeUnit.toMillis(this.ttl);
    }

    protected static class InnerEntry<K, V> implements Entry<K, V> {
        final int hash;

        final K key;

        V value;

        InnerEntry<K, V> next;

        InnerEntry(int hash, K key, V value, InnerEntry<K, V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        private InnerEntry(K key, V value) {
            this.hash = Objects.hash(key, value);
            this.key = key;
            this.value = value;
            this.next = null;
        }

        public final K getKey() {
            return this.key;
        }

        public final V getValue() {
            return this.value;
        }

        public final String toString() {
            return (new StringBuilder()).append(this.key).append("=").append(this.value).toString();
        }

        public final boolean hasNext() {
            return (this.next != null);
        }

        public final InnerEntry<K, V> next() {
            return this.next;
        }

        public final int hashCode() {
            return Objects.hashCode(this.key) ^ Objects.hashCode(this.value);
        }

        public final V setValue(V newValue) {
            V oldValue = this.value;
            this.value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Entry<?, ?> e = (Entry<?, ?>) o;
                return (Objects.equals(this.key, e.getKey()) &&
                        Objects.equals(this.value, e.getValue()));
            }
            return false;
        }
    }

    public static class Builder<K, V> {
        private Class<? extends Map> innerMapClass;

        protected TimeUnit timeUnit;

        protected long ttl;

        private final Logger logger = LoggerFactory.getLogger(Builder.class);

        public Builder() {
            this.innerMapClass = TTLMap.DEFAULT_INNER_MAP_CLASS;
            this.timeUnit = TTLMap.DEFAULT_TIME_UNIT;
            this.ttl = 10L;
        }

        public Builder<K, V> innerMapClass(Class<? extends Map> innerMapClass) {
            Assert.notNull(innerMapClass, "error.not_null");
            this.innerMapClass = innerMapClass;
            return this;
        }

        public Builder<K, V> timeUnit(TimeUnit timeUnit) {
            Assert.notNull(timeUnit, "error.not_null");
            this.timeUnit = timeUnit;
            return this;
        }

        public Builder<K, V> ttl(long ttl) {
            Assert.isTrue((ttl >= 0L), "error.data_invalid");
            this.ttl = ttl;
            return this;
        }

        public TTLMap<K, V> build() {
            TTLMap<K, V> ttlMap = new TTLMap<>(true);
            try {
                ttlMap.innerMap = this.innerMapClass.newInstance();
                ttlMap.timeUnit = this.timeUnit;
                ttlMap.ttl = this.ttl;
                ttlMap.ttlInMillisecond = ttlMap.timeUnit.toMillis(ttlMap.ttl);
            } catch (InstantiationException | IllegalAccessException e) {
                this.logger.error("can not build TTLMap with class {}, fallback to default builder", this.innerMapClass.getName());
                this.logger.error(e.getMessage(), e);
                ttlMap = new TTLMap<>();
            }
            return ttlMap;
        }
    }
}
